#!/usr/bin/python

# General python modules required for game execution
import pygame, weakref, gc

from pygame.locals import Rect, Color
from pygame import draw, transform, Surface

# Local module imports of the Pyxel sub-systems
from library import Resource, xml_map
from options import options, args
from utils import text2dict, indentify
from interface import Label, Dialog, Style
from sprites import Sprite, Target
from elements import Room, Prop, Element, Exit, NPC
from event import Event

# Initialize Pygame
pygame.init()

# Display and control visual game interface
# This class only has class methods, and must not be instantiated
class Game(object):
        
    # Initialize Game Engine
    @classmethod
    def load(cls, game, dir_path = None):
        
        # Initialize script execution enviroment
        cls.exec_globals = {'Game': cls, 'Resource': Resource, 'Target': Target, \
                            'Event': Event, 'Element': Element, 'Dialog': Dialog, 'NPC': NPC}
     
        # Initialize game Resources and initial configuration
        Resource.library_init(game, dir_path = dir_path)
        config = Resource("config.xml")
        settings = config.find("settings")
        modules = config.find("modules")
        
        # Obtain the game defined videomode
        window = settings.find("window")
        cls.videomode = int(window.attrib['width']), int(window.attrib['height'])
        
        # Initialize the display
        cls.screen = pygame.display.set_mode(cls.videomode)
        pygame.display.set_caption(settings.find("title").text)    
        
        # Setup the rendering layer order
        cls.layers = (NPC, Prop, Dialog, Exit, Label)    
        
        # Set up GUI styles
        Style.parse(Resource('style.css').read())
        
        # Initialize interface elements
        cls.waiting = pygame.sprite.Group()
        def retrieve(c, t, m = modules): 
            return xml_map(Resource(m.find(c).attrib["file"]), t)
                        
        # Load initial game map and room states 
        cls.map = retrieve('map', 'room')
        cls.location = None
        cls.inventory = retrieve('inv', 'item')
        
        # Load NPCs and the NPC map
        cls.npc = retrieve('cast', 'npc')
        cls.npc_map = dict((i, []) for i in cls.map)
        for node in cls.npc.values(): 
            if 'start' in node.attrib:
                cls.npc_map[node.attrib['start']].append(NPC(node))
                    
        # Move to initial default map
        cls.warp('__root__')

    # Actually runs the game in a loop
    # Note: Try to avoid deep nested or excesive calls and keep it tight
    @classmethod
    def play(cls):
        
        pygame.event.set_blocked((pygame.VIDEORESIZE))
        screen = cls.screen
        cls.clock = pygame.time.Clock()
        target = None
        prev_click = None
        
        # Initialize the player's cursor
        pygame.mouse.set_visible(False)
        cursor = Sprite("cursor.png", (400, 300))
                
        while True:
            
            # Needed to keep everything running smoothly
            pygame.event.pump() 
            lapse = cls.clock.tick(40)
            
            # Event processing: the backbone of the engine
            for event in pygame.event.get():
                if event.type == pygame.QUIT: return
                elif event.type == pygame.MOUSEMOTION: 
                    cursor.rect = event.pos
                    focus = Target.at(event.pos)
                    if focus is not None:
                        if focus not in Target.focus:
                            if Target.focus.sprite:
                                Target.focus.sprite.leave()
                            Target.focus.add(focus)
                            focus.hover()
                    else:
                        if Target.focus.sprite:
                            Target.focus.sprite.leave()
                        Target.focus.empty()
                    if target is not None:
                        target.drag(event.pos)
                    focus = None
                elif event.type == pygame.MOUSEBUTTONDOWN:
                    target = Target.at(event.pos)
                    if target is not None:
                        if event.button == 1:
                            if target.draggable:
                                Target.grab.add(target)
                                Target.focus.empty()
                                prev_click = event.pos[:]
                            else:
                                pos = target.rect.topleft
                                target.click((event.pos[0] - pos[0], event.pos[1] - pos[1]))
                                target = None
                        else:
                            target = None         
                elif event.type == pygame.MOUSEBUTTONUP:
                    if target is not None:
                        if prev_click == event.pos:
                            pos = target.rect.topleft
                            target.click((event.pos[0] - pos[0], event.pos[1] - pos[1]))
                        else:
                            print "Drop on ", Target.focus.sprite
                            target.drop(Target.focus.sprite)
                        target = None
                        prev_click = None
                        Target.focus.add(Target.grab.sprite)
                        Target.grab.empty()
                elif event.type == pygame.KEYDOWN:
                    if event.key in (pygame.K_SPACE, pygame.K_RETURN):
                        if cls.__auto_forward: cls.forward()
            
            # Don't execute exit triggers or enqueued events
            # as long as a sprite is held on the move waiting list
            
            if True: #not cls.waiting:
                        
                # Execute any pending Exit triggers
                if Exit.trigger is not None:
                    cls.warp(Exit.trigger); Exit.trigger = None        
                
                # Execute any enqueued events
                Event.resolve(cls.exec_globals)
                    
            else:
                cls.waiting = filter(lambda x: x.moving(), cls.waiting)
                            
                
            # Draw the room background
            screen.blit(cls.location.bg, (0,0))                
                
            # Update and draw game elements
            for spr in cls.layers:
                spr.group.update(lapse)
                spr.group.draw(screen)
                                    
            # Render the player controlled cursor
            screen.blit(cursor.image, cursor.rect)
            pygame.display.update()
        

    # Parse an xml 'event' element and execute the proper actions requested
    @classmethod
    def execute(cls, event_id, context):

        print "Execute:", event_id, context
        
        # Assert that event_id is a string id and context is a valid Element instance
        assert isinstance(context, Element), "Context must be a valid element"
        assert isinstance(event_id, basestring), "event name must be a string"

        # Fetch the events from the given context
        ev_list = context.event.get(event_id, None)

        # Fetch state data if context supports it
        exec_locals = context.state.copy() if 'state' in context else dict()

        # If event context does not contain the requested event, skip execution
        if not ev_list: return
        for ev in ev_list: 
                            
            # Queue the event for execution
            print ev.trigger, ev.actions
            ev.process(cls.exec_globals, exec_locals)
                                       
    # Move player position to a designated room in the map
    @classmethod
    def warp(cls, room):
        
        # Verify that or input is a string identifier
        assert isinstance(room, basestring), "Room must be a string id"
        assert room in cls.map, "Room " + room + " does not exist."
        
        # Clear the current cursor focus
        if Target.focus: Target.focus.empty()
        Dialog.group.empty()
        
        # Execute 'leave' event on previous location if applicable
        if cls.location is not None: 
            for npc in NPC.group: cls.execute("part", npc)
            NPC.group.empty()
            Prop.group.empty()
            cls.execute("leave", cls.location)
        
        # Load the Room to memory
        cls.location = Room(cls.map[room])
        
        # Execute room arrival events if applicable
        cls.execute("arrive", cls.location)
        
        # Load all NPCs in the current room
        for npc in cls.npc_map[room]: 
            npc.show(); cls.execute("meet", npc)


        
    # Find a NPC in the map. Returns NPC instance and container room id
    # if NPC is not in map, returns None twice
    @classmethod
    def locate(cls, id):
        location = None
        for location in cls.map:
            for npc in cls.npc_map[location]:
                if npc.id == id: 
                    return npc, location
        return None, None
        
    # Summon an NPC to the current specified location (defaults to current room)
    # This will throw an exception if self.location is undefined
    @classmethod
    def summon(cls, id, room = None):
        here = cls.location.id
        if room is None: room = here
        else: assert room in cls.map.keys(), "Invalid room string id"
        
        npc, location = cls.locate(id)
        width = cls.videomode[0]
        
        # First, see if the NPC is already loaded
        if location is not None:
            if location == room: return # The NPC is already in the room
            cls.npc_map[location].remove(npc)
            if location == here:
                if npc.rect >= width // 2:
                    point = (npc.rect.centerx + width, npc.rect.centery)
                else:
                    point = (-npc.rect.width, npc.rect.centery)
                npc.sweep(point, 500)
                cls.waiting.add(npc)
                cls.execute("part", npc)
        else:
            npc = NPC(cls.npc[id])
            
        cls.npc_map[room].append(npc)
        npc.location = room
        if room == here:
            if npc.rect >= width // 2:
                point = (npc.rect.centerx + width, npc.rect.centery)
            else:
                point = (-npc.rect.width, npc.rect.centery)
            npc.nudge(point, 500)
            cls.waiting.add(npc)
            npc.show(); cls.execute('meet', npc)
            
        return npc
            
    # Remove a NPC from the Map 
    @classmethod
    def dismiss(cls, id):
        npc, location = cls.locate(id)
        if npc is not None:
            if npc.location == cls.location.id:
                width = cls.videomode[0]
                if npc.rect >= width // 2:
                    point = (npc.rect.centerx + width, npc.rect.centery)
                else:
                    point = (-npc.rect.width, npc.rect.centery)
                npc.sweep(point, 500)
                cls.waiting.add(npc)
                cls.execute("part", npc)
            npc.location = None
            cls.npc_map[location].remove(npc)
                        
           
        
# When executed from the command line, it runs the specified game directly
if __name__ == "__main__":
    Game.load(options.game, options.dir_path)
    Game.play()
        
        




